Skip to content

fix(t3x): auto-resume never fired — 'thread-advanced' false cancellation on settled turns - #9

Merged
radroid merged 1 commit into
mainfrom
t3x/fix-thread-advanced-false-cancel
Jul 25, 2026
Merged

fix(t3x): auto-resume never fired — 'thread-advanced' false cancellation on settled turns#9
radroid merged 1 commit into
mainfrom
t3x/fix-thread-advanced-false-cancel

Conversation

@radroid

@radroid radroid commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Fixes #6.

What was broken

Every real-world auto-resume was cancelled at wake time with "Auto-resume cancelled: thread-advanced." — observed 3/3 on 2026-07-25 (threads 33c06a5a, 8f31df69, bb66acc2, all cancelled within 2s of their shared window reset), and the durable state showed firedAtMs: [] everywhere: the feature had never successfully fired.

Root cause

ProjectionSnapshotQuery.getSnapshot() reads threads from the SQLite projection, where latestTurn is joined on projection_threads.latest_turn_id — a column populated only while a turn is active. A usage limit normally lands mid-turn, so the guard baseline captures the running turn's id; by wake time the turn has settled and the snapshot reports latestTurn: null. The guard

if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";

read null !== "<turnId>" as advancement and cancelled — then nothing ever re-armed, because an idle thread emits no further rejection events.

The fix

thread-advanced now requires positive evidence — a different, non-null turn id:

const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) return "thread-advanced";

null at fire time means "no active turn", the expected state after a settled limit. Genuine user takeovers remain covered by user-took-over (checked first, keyed on the newest user message); active work remains covered by progressing. Known residual: an auto-started turn that both runs and settles during the wait with no user message is no longer detected via turn id — acceptable against a guard that previously cancelled 100% of legitimate resumes.

Testing

  • New unit case (guards.test.ts): fire-time latestTurn: null vs non-null baseline → no cancellation; plus the converse (baseline null, turn present → still cancels).
  • New integration case (Reactor.test.ts): replays the production incident — rejection arrives mid-running-turn, thread settles to latestTurn: null + session stopped during the wait, wake must dispatch exactly one resume turn and post no thread-advanced note.
  • Both verified to fail against the unfixed guard (stash-run-restore), then pass with the fix.
  • Full src/t3x/autoResume suite: 69/69 across 4 consecutive runs. pnpm typecheck output identical to main (two pre-existing non-error diagnostics in untouched files).
  • Diagnosis evidence (timeline, event-store replay, live DB columns) recorded in Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait #6.

🤖 Generated with Claude Code

…d turn settles

The projection populates projection_threads.latest_turn_id only while a turn
is active, so a usage limit that lands mid-turn captures the running turn's id
in the guard baseline — and by wake time (turn settled, session idle) the
snapshot reports latestTurn: null. cancelReason treated that null as evidence
the thread had advanced and cancelled the pending resume, which killed every
real-world resume (observed 3/3 on 2026-07-25; firedAtMs was empty across all
threads — the feature had never fired).

Advancement now requires positive evidence: a different, NON-NULL turn id.
Null means "no active turn" — the expected state after a settled limit. User
takeovers stay covered by the user-took-over guard (checked first) and active
work by the progressing guard.

Regression coverage: a guards.test.ts unit case (null-at-fire vs non-null
baseline must not cancel) plus a Reactor.test.ts integration case replaying
the production shape (schedule mid-running-turn, settle to latestTurn: null,
assert the resume fires with no thread-advanced note). Both verified to fail
against the previous guard.

Fixes #6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9a36a907-aa9f-4ca4-bf0e-1e8885209e18

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3x/fix-thread-advanced-false-cancel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit 237ca66 into main Jul 25, 2026
1 check passed
@radroid
radroid deleted the t3x/fix-thread-advanced-false-cancel branch July 25, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-resume never fires in practice: 'thread-advanced' false cancellation when the limited turn settles during the wait

1 participant